##
# The Bottle where the experiment is carried on.
# For specific attributes, please refer to +Bottle#initialize+ definition.
#
# By default using the +Bottle#update+ method to simulate the next frame.
# It will automatically output the +@temp+ data unless output is preserved.
#
# The simulate physics law is written by frame:
# * +dQ1 = k * (@temp - @env_temp) * @dt+ where +k+ is considered +1+
#   and +dQ1+ stands for conduct heat.
# * +dQ2 = @heater ? @watt * @dt : 0+ stands for heat by heater
# * +dQ = dQ1 + dQ2+
# * +@temp -= dQ / @heat_capacity+
#
# And using the +Bottle#on+ and +Bottle#off+ method to switch on and off
# the heater. Also, +Bottle#switch+ will inverse the heater status.
#
# You can pass in procs to +env_temp_func+ and +expect_temp_f+
# to simulate the environment changes during the time.
# The passed procs will be called when the +update+ method is
# called. And +@t+ will be passed in, they should return a +Float+
# value if possible.
class Bottle
  attr_accessor :temp, :refresh, :dt, :max_watt, :expect_temp, :env_temp
  attr_reader :t, :heater, :heat_capacity, :refresh, :dt, :watt

  # Use +Bottle#watt=+ method to assign watt to heater.
  # Assigned value will only be avaliable if they are in valid range.
  def watt=(watt)
    @watt = watt < @min_watt ? 0 : watt > @max_watt ? @max_watt : watt
  end

  # This returns a new +Bottle+ instance.
  # The attributes can be passed in in forms of key-value pairs like:
  #
  #   Bottle.new(temp: 27.0, env_temp: 30)
  def initialize(options = {})
    @env_temp      = (options[:env_temp] || 20.0).to_f        # Environment's temperature
    @expect_temp   = (options[:expect_temp] || 40.0).to_f     # Expect Bottle Temperture
    @min_watt      = (options[:min_watt] || 0.0).to_f         # Min Watt
    @max_watt      = (options[:max_watt] || 50.0).to_f        # Max Watt
    @temp          = (options[:temp] || 16.0).to_f            # Bottle's temperature
    @heater        = options[:heater] || false                # Heater status
    @watt          = (options[:watt] || 50.0).to_f            # Heater Power Watt
    @heat_capacity = (options[:heat_capacity] || 10.0).to_f   # Bottle Heat Capacity
    @refresh       = (options[:refresh] || 100).to_i          # Output Refresh Frequence
    @dt            = (options[:dt]      || 0.01).to_f         # Simulated Frame Time
    @counter       = 0                                        # Counter for Frame
    @t             = 0                                        # Simulated Time
    @env_temp_func = (options[:env_temp_func] || nil)         # Environment Temp Function
    @expect_temp_f = (options[:expect_temp_f] ||              # Expect Temp Function
                      options[:expect_temp_func] || nil)
  end

  # If a block is passed in, it will yield itself for easy controlling.
  # 
  # Update Bottle status by simulate.
  # The simulate physics law is written by frame:
  # * +dQ1 = k * (@temp - @env_temp) * @dt+ where +k+ is considered +1+
  #   and +dQ1+ stands for conduct heat.
  # * +dQ2 = @heater ? @watt * @dt : 0+ stands for heat by heater
  # * +dQ = dQ1 + dQ2+
  # * +@temp -= dQ / @heat_capacity+
  def update(&block)
    @counter = (@counter + 1) % @refresh
    puts "#{@t.round(2)}\t#{@temp.round(2)}" if @counter == 1
    
    @t += @dt
    @env_temp = @env_temp_func.call(@t).to_f if @env_temp_func.respond_to? :call
    @expect_temp = @expect_temp_f.call(@t).to_f if @expect_temp_f.respond_to? :call

    yield(self) if block
    
    dQ1 = (@temp - @env_temp) * @dt
    dQ2 = @heater ? @watt * @dt : 0
    dQ  = dQ1 - dQ2
    
    @temp -= dQ / @heat_capacity
  end

  # Switch heater on
  def on
    @heater = true
  end

  # Switch heater off
  def off
    @heater = false
  end

  # Switch heater status
  def switch
    @heater = @heater ? false : true
  end
end
